153 lines · 6.6 KB
| 1 | --- |
| 2 | import Repo from '../../../layouts/Repo.astro'; |
| 3 | import { apiGet, apiPost } from '../../../lib/api'; |
| 4 | |
| 5 | const { owner, repo } = Astro.params; |
| 6 | const cookie = Astro.request.headers.get('cookie') || ''; |
| 7 | |
| 8 | let repoData: any = null; |
| 9 | let webhooks: any[] = []; |
| 10 | let error = ''; |
| 11 | let success = ''; |
| 12 | |
| 13 | try { |
| 14 | repoData = await apiGet(`/api/repos/${owner}/${repo}`, cookie); |
| 15 | try { |
| 16 | webhooks = await apiGet(`/api/repos/${owner}/${repo}/webhooks`, cookie); |
| 17 | } catch { /* may not have access */ } |
| 18 | } catch (e: any) { |
| 19 | error = e.message; |
| 20 | } |
| 21 | |
| 22 | if (Astro.request.method === 'POST') { |
| 23 | try { |
| 24 | const formData = await Astro.request.formData(); |
| 25 | const action = formData.get('action'); |
| 26 | |
| 27 | if (action === 'update') { |
| 28 | const description = formData.get('description') as string; |
| 29 | const isPrivate = formData.get('visibility') === 'private'; |
| 30 | |
| 31 | const res = await fetch( |
| 32 | `${import.meta.env.PUBLIC_API_URL || 'http://localhost:8321'}/api/repos/${owner}/${repo}/settings`, |
| 33 | { |
| 34 | method: 'PUT', |
| 35 | headers: { 'Content-Type': 'application/json', Cookie: cookie }, |
| 36 | body: JSON.stringify({ description, is_private: isPrivate }), |
| 37 | } |
| 38 | ); |
| 39 | if (!res.ok) throw new Error('Failed to update'); |
| 40 | success = 'Settings updated.'; |
| 41 | repoData = await res.json(); |
| 42 | } else if (action === 'add_webhook') { |
| 43 | const url = formData.get('webhook_url') as string; |
| 44 | const secret = formData.get('webhook_secret') as string; |
| 45 | const events = formData.get('webhook_events') as string || 'push'; |
| 46 | await apiPost(`/api/repos/${owner}/${repo}/webhooks`, { url, secret, events }, cookie); |
| 47 | success = 'Webhook added.'; |
| 48 | webhooks = await apiGet(`/api/repos/${owner}/${repo}/webhooks`, cookie); |
| 49 | } |
| 50 | |
| 51 | return Astro.redirect(`/${owner}/${repo}/settings`); |
| 52 | } catch (e: any) { |
| 53 | error = e.message; |
| 54 | } |
| 55 | } |
| 56 | --- |
| 57 | |
| 58 | <Repo owner={owner!} repo={repo!} activeTab="settings"> |
| 59 | {error && <div class="flash-error">{error}</div>} |
| 60 | {success && <div style="background: var(--success-bg); border: 1px solid var(--success-border); padding: 12px; border-radius: var(--radius); margin-bottom: 16px; font-size: 0.875rem; color: var(--success-text);">{success}</div>} |
| 61 | |
| 62 | {repoData && ( |
| 63 | <> |
| 64 | {/* General settings */} |
| 65 | <div class="card" style="margin-bottom: 24px;"> |
| 66 | <h2 style="font-size: 1.125rem; margin-bottom: 16px;">General</h2> |
| 67 | <form method="POST"> |
| 68 | <input type="hidden" name="action" value="update" /> |
| 69 | <div class="form-group"> |
| 70 | <label for="description">Description</label> |
| 71 | <input type="text" id="description" name="description" value={repoData.description || ''} /> |
| 72 | </div> |
| 73 | <div class="form-group"> |
| 74 | <label>Visibility</label> |
| 75 | <div style="display: flex; gap: 16px; margin-top: 4px;"> |
| 76 | <label style="display: flex; align-items: center; gap: 6px; font-weight: normal; cursor: pointer;"> |
| 77 | <input type="radio" name="visibility" value="public" checked={!repoData.is_private} /> Public |
| 78 | </label> |
| 79 | <label style="display: flex; align-items: center; gap: 6px; font-weight: normal; cursor: pointer;"> |
| 80 | <input type="radio" name="visibility" value="private" checked={!!repoData.is_private} /> Private |
| 81 | </label> |
| 82 | </div> |
| 83 | </div> |
| 84 | <button type="submit" class="btn btn-primary">Save changes</button> |
| 85 | </form> |
| 86 | </div> |
| 87 | |
| 88 | {/* Clone info */} |
| 89 | <div class="card" style="margin-bottom: 24px;"> |
| 90 | <h2 style="font-size: 1.125rem; margin-bottom: 12px;">Clone URL</h2> |
| 91 | <code style="display: block; background: var(--bg); padding: 8px 12px; border-radius: var(--radius); border: 1px solid var(--border); font-size: 0.8125rem;"> |
| 92 | git clone {new URL(Astro.request.url).origin}/{owner}/{repo}.git |
| 93 | </code> |
| 94 | </div> |
| 95 | |
| 96 | {/* Webhooks */} |
| 97 | <div class="card" style="margin-bottom: 24px;"> |
| 98 | <h2 style="font-size: 1.125rem; margin-bottom: 12px;">Webhooks</h2> |
| 99 | |
| 100 | {webhooks.length > 0 && ( |
| 101 | <div style="margin-bottom: 16px;"> |
| 102 | {webhooks.map((wh: any) => ( |
| 103 | <div style="display: flex; justify-content: space-between; align-items: center; padding: 8px 0; border-bottom: 1px solid var(--border); font-size: 0.875rem;"> |
| 104 | <div> |
| 105 | <code>{wh.url}</code> |
| 106 | <span style="color: var(--text-muted); margin-left: 8px; font-size: 0.75rem;"> |
| 107 | Events: {wh.events} |
| 108 | </span> |
| 109 | </div> |
| 110 | <span style={`font-size: 0.75rem; ${wh.is_active ? 'color: var(--accent);' : 'color: var(--danger);'}`}> |
| 111 | {wh.is_active ? 'active' : 'inactive'} |
| 112 | </span> |
| 113 | </div> |
| 114 | ))} |
| 115 | </div> |
| 116 | )} |
| 117 | |
| 118 | <form method="POST"> |
| 119 | <input type="hidden" name="action" value="add_webhook" /> |
| 120 | <div class="form-group"> |
| 121 | <label for="webhook_url">Payload URL</label> |
| 122 | <input type="text" id="webhook_url" name="webhook_url" required placeholder="https://example.com/webhook" /> |
| 123 | </div> |
| 124 | <div style="display: flex; gap: 8px;"> |
| 125 | <div class="form-group" style="flex: 1;"> |
| 126 | <label for="webhook_secret">Secret <span style="color: var(--text-muted); font-weight: normal;">(optional)</span></label> |
| 127 | <input type="text" id="webhook_secret" name="webhook_secret" placeholder="HMAC signing secret" /> |
| 128 | </div> |
| 129 | <div class="form-group" style="flex: 1;"> |
| 130 | <label for="webhook_events">Events</label> |
| 131 | <input type="text" id="webhook_events" name="webhook_events" value="push,merge_request" /> |
| 132 | </div> |
| 133 | </div> |
| 134 | <button type="submit" class="btn btn-primary">Add webhook</button> |
| 135 | </form> |
| 136 | </div> |
| 137 | |
| 138 | {/* Danger zone */} |
| 139 | <div style="border: 1px solid var(--danger); border-radius: var(--radius); padding: 16px;"> |
| 140 | <h2 style="font-size: 1.125rem; color: var(--danger); margin-bottom: 8px;">Danger Zone</h2> |
| 141 | <p style="font-size: 0.8125rem; color: var(--text-muted); margin-bottom: 12px;"> |
| 142 | Once you delete a repository, there is no going back. |
| 143 | </p> |
| 144 | <button class="btn" style="border-color: var(--danger); color: var(--danger);" |
| 145 | onclick="if(confirm('Are you sure? This cannot be undone.')) { fetch(this.dataset.url, {method:'DELETE',credentials:'include'}).then(()=>location.href='/') }" |
| 146 | data-url={`${import.meta.env.PUBLIC_API_URL || 'http://localhost:8321'}/api/repos/${owner}/${repo}`}> |
| 147 | Delete this repository |
| 148 | </button> |
| 149 | </div> |
| 150 | </> |
| 151 | )} |
| 152 | </Repo> |
| 153 |